// MvcRouteUnitTester
// by Lee Dumond 
// twitter.com/LeeDumond

using System.Web;

namespace MvcRouteUnitTester
{
   using System;
   using System.Collections.Generic;
   using System.Linq;
   using System.Reflection;
   using System.Web.Mvc;
   using System.Web.Routing;

   /// <summary>
   /// An object which encapsulates request information.
   /// </summary>
   public class RequestInfo
   {
      private readonly string requestUrl;
      //private readonly RouteData routeData;
      private bool areaFlag;
      private readonly RouteCollection applicationRoutes;
      

      internal RequestInfo(RouteCollection applicationRoutes, string url, string httpMethod)
      {
         HttpContext = TestUtility.GetHttpContext(PrepareUrl(url), httpMethod);
         this.applicationRoutes = applicationRoutes;
         //routeData = applicationRoutes.GetRouteData(httpContext);
         requestUrl = url;
      }

      /// <summary>
      /// The mocked HTTP context for the test to be performed.
      /// </summary>
      public HttpContextBase HttpContext { get; set; }
      

      /// <summary>
      /// Asserts that the area routing information supplied to the method would be matched by the given request.
      /// </summary>
      /// <param name="expectedArea">The name of the area that is expected to be matched.</param>
      /// <param name="expectedController">The name of the controller that is expected to be matched.</param>
      /// <param name="expectedAction">The name of the action that is expected to be matched.</param>
      /// <param name="expectedRouteValues">An anonymous object containing URL parameter values and default route values that are expected to be matched.</param>
      /// <exception cref="ArgumentException">Thrown when either the <paramref name="expectedArea"/>, <paramref name="expectedController"/>, or <paramref name="expectedAction"/> argument is null, empty, or contains only whitespace.</exception>
      /// <exception cref="AssertionException">Thrown when any mismatch is found between the supplied information and the given request.</exception>
      public void ShouldMatchRoute(string expectedArea, string expectedController, string expectedAction,
                                   object expectedRouteValues = null)
      {
         if (string.IsNullOrWhiteSpace(expectedArea))
         {
            throw new ArgumentException(
               "Area cannot be null or empty. If you are testing non-area routes, use the overload of MatchesRoute that does not require an area argument.",
               "expectedArea");
         }

         var routeData = applicationRoutes.GetRouteData(HttpContext);

         if (!ValueCompare(routeData.DataTokens["area"], expectedArea))
         {
            throw new AssertionException(
               string.Format("Area name mismatch. Expected: \"{0}\", but was: \"{1}\" (for url: \"{2}\").", expectedArea,
                             routeData.DataTokens["area"], requestUrl));
         }

         areaFlag = true;
         ShouldMatchRoute(expectedController, expectedAction, expectedRouteValues);
      }

      /// <summary>
      /// Asserts that the routing information supplied to the method would be matched by the given request.
      /// </summary>
      /// <param name="expectedController">The name of the controller that is expected to be matched</param>
      /// <param name="expectedAction">The name of the action that is expected to be matched.</param>
      /// <param name="expectedRouteValues">An anonymous object containing URL parameter values and default route values that are expected to be matched.</param>
      /// <exception cref="ArgumentException">Thrown when either the <paramref name="expectedController"/>, or <paramref name="expectedAction"/> argument is null, empty, or contains only whitespace.</exception>
      /// <exception cref="AssertionException">Thrown when any mismatch is found between the supplied information and the given request.</exception>
      public void ShouldMatchRoute(string expectedController, string expectedAction, object expectedRouteValues = null)
      {
         if (string.IsNullOrWhiteSpace(expectedController))
         {
            throw new ArgumentException("Controller cannot be null or empty.", "expectedController");
         }

         if (string.IsNullOrWhiteSpace(expectedAction))
         {
            throw new ArgumentException("Action cannot be null or empty.", "expectedAction");
         }

         var routeData = applicationRoutes.GetRouteData(HttpContext);

         if (routeData != null)
         {
            if (routeData.DataTokens["area"] != null && !areaFlag)
            {
               throw new AssertionException(
                  string.Format("Area name mismatch. Expected: \"\", but was: \"{0}\" (for url: \"{1}\").",
                                routeData.DataTokens["area"], requestUrl));
            }

            if (!ValueCompare(expectedController, routeData.Values["controller"]))
            {
               throw new AssertionException(
                  string.Format("Controller name mismatch. Expected: \"{0}\", but was: \"{1}\" (for url: \"{2}\").",
                                expectedController, routeData.Values["controller"], requestUrl));
            }

            if (!ValueCompare(expectedAction, routeData.Values["action"]))
            {
               throw new AssertionException(
                  string.Format("Action name mismatch. Expected: \"{0}\", but was: \"{1}\" (for url: \"{2}\").",
                                expectedAction, routeData.Values["action"], requestUrl));
            }

            Dictionary<string, object> actualRouteValuesDictionary = routeData.Values
               .Where(
                  v => v.Key != "controller" && v.Key != "action" && v.Value != UrlParameter.Optional)
               .ToDictionary(p => p.Key, p => p.Value);

            if (actualRouteValuesDictionary.Count > 0)
            {
               if (expectedRouteValues == null)
               {
                  throw new AssertionException(
                     string.Format(
                        "Route values mismatch. Expected: 0 route values, but was: {0} route values (for url: \"{1}\").",
                        actualRouteValuesDictionary.Count, requestUrl));
               }

               Dictionary<string, object> expectedRouteValuesDictionary = BuildDictionary(expectedRouteValues);

               if (expectedRouteValuesDictionary.Count >= actualRouteValuesDictionary.Count)
               {
                  foreach (var entry in expectedRouteValuesDictionary)
                  {
                     if (!actualRouteValuesDictionary.ContainsKey(entry.Key))
                     {
                        throw new AssertionException(
                           string.Format(
                              "Route values mismatch. Expected route value with key \"{0}\" was not found (for url: \"{1}\").",
                              entry.Key,
                              requestUrl));
                     }

                     if (!ValueCompare(actualRouteValuesDictionary[entry.Key], expectedRouteValuesDictionary[entry.Key]))
                     {
                        throw new AssertionException(
                           string.Format(
                              "Route values mismatch. Expected: route value with key \"{0}\" and value \"{1}\", but was: route value with key \"{0}\" and value \"{2}\" (for url: \"{3}\").",
                              entry.Key, expectedRouteValuesDictionary[entry.Key],
                              actualRouteValuesDictionary[entry.Key], requestUrl));
                     }
                  }
               }
               else
               {
                  foreach (var entry in actualRouteValuesDictionary)
                  {
                     if (!expectedRouteValuesDictionary.ContainsKey(entry.Key))
                     {
                        throw new AssertionException(
                           string.Format(
                              "Route values mismatch. Unexpected route value with key \"{0}\" and value \"{1}\" was found (for url: \"{2}\").",
                              entry.Key, actualRouteValuesDictionary[entry.Key], requestUrl));
                     }

                     if (!ValueCompare(actualRouteValuesDictionary[entry.Key], expectedRouteValuesDictionary[entry.Key]))
                     {
                        throw new AssertionException(
                           string.Format(
                              "Route values mismatch. Expected: route value with key \"{0}\" and value \"{1}\", but was: route value with key \"{0}\" and value \"{2}\" (for url: \"{3}\").",
                              entry.Key, expectedRouteValuesDictionary[entry.Key],
                              actualRouteValuesDictionary[entry.Key], requestUrl));
                     }
                  }
               }
            }
            else
            {
               if (expectedRouteValues != null)
               {
                  throw new AssertionException(
                     string.Format(
                        "Route values mismatch. Expected: {0} route values, but was: 0 route values (for url: \"{1}\").",
                        expectedRouteValues.GetType().GetProperties().Count(), requestUrl));
               }
            }
         }
         else
         {
            throw new AssertionException(string.Format("No matching route was found (for url: \"{0}\").", requestUrl));
         }
      }

      /// <summary>
      /// Indicates whether the area routing information supplied to the method would be matched by the given request.
      /// </summary>
      /// <param name="expectedArea">The name of the area that is expected to be matched.</param>
      /// <param name="expectedController">The name of the controller that is expected to be matched.</param>
      /// <param name="expectedAction">The name of the action that is expected to be matched.</param>
      /// <param name="expectedRouteValues">An anonymous object containing URL parameter values and default route values that are expected to be matched.</param>
      /// <exception cref="ArgumentException">Thrown when either the <paramref name="expectedArea"/>, <paramref name="expectedController"/>, or <paramref name="expectedAction"/> argument is null, empty, or contains only whitespace.</exception>
      /// <returns>True if the area routing information supplied to the method would be matched by the given request; otherwise false.</returns>
      [Obsolete("Use native assertion method ShouldMatchRoute instead.")]
      public bool MatchesRoute(string expectedArea, string expectedController, string expectedAction,
                               object expectedRouteValues = null)
      {         
         try
         {
            ShouldMatchRoute(expectedArea, expectedController, expectedAction, expectedRouteValues);
            // Passed assertions without exception means it was a match.
            return true;
         }
         catch (AssertionException)
         {
            return false;
         }
      }

      /// <summary>
      /// Indicates whether the routing information supplied to the method would be matched by the given request.
      /// </summary>
      /// <param name="expectedController">The name of the controller that is expected to be matched.</param>
      /// <param name="expectedAction">The name of the action that is expected to be matched.</param>
      /// <param name="expectedRouteValues">An anonymous object containing URL parameter values and default route values that are expected to be matched.</param>
      ///<exception cref="ArgumentException">Thrown when either the <paramref name="expectedController"/>, or <paramref name="expectedAction"/> argument is null, empty, or contains only whitespace. </exception>
      /// <returns>True if the routing information supplied to the method would be matched by the given request; otherwise false.</returns>
      [Obsolete("Use native assertion method ShouldMatchRoute instead.")]
      public bool MatchesRoute(string expectedController, string expectedAction, object expectedRouteValues = null)
      {
         try
         {
            ShouldMatchRoute(expectedController, expectedAction, expectedRouteValues);
            // Passed assertions without exception means it was a match.
            return true;
         }
         catch (AssertionException)
         {
            return false;
         }
      }

      /// <summary>
      /// Asserts that no routes would be matched by the given request.
      /// </summary>
      /// <exception cref="AssertionException">Thrown if any route matches the given request.</exception>
      public void ShouldMatchNoRoute()
      {
         var routeData = applicationRoutes.GetRouteData(HttpContext);

         if (routeData != null && routeData.Route != null)
         {
            throw new AssertionException(string.Format("A matching route was found (for url: \"{0}\").", requestUrl));
         }
      }

      /// <summary>
      /// Indicates whether any routes would be matched by the given request.
      /// </summary>
      /// <returns>True if no routes are matched by the given request; otherwise false.</returns>
      [Obsolete("Use native assertion method ShouldMatchNoRoute instead.")]
      public bool MatchesNoRoute()
      {
         try
         {
            ShouldMatchNoRoute();
            // Passed assertions without exception means it was a match.
            return true;
         }
         catch (AssertionException)
         {
            return false;
         }
      }

      /// <summary>
      /// Asserts that the given request is ignored by the routing system.
      /// </summary>
      /// <exception cref="AssertionException">Thrown if given request is not ignored by the routing system.</exception>
      public void ShouldBeIgnored()
      {
         var routeData = applicationRoutes.GetRouteData(HttpContext);

         if (!(routeData.RouteHandler is StopRoutingHandler))
         {
            throw new AssertionException(string.Format("The request was not ignored (for url: \"{0}\").",
                                                       requestUrl));
         }
      }

      /// <summary>
      /// Indicates whether the given request is ignored by the routing system.
      /// </summary>
      /// <returns>True if the given request is ignored by the routing system; otherwise false.</returns>
      [Obsolete("Use native assertion method ShouldBeIgnored instead.")]
      public bool IsIgnored()
      {
         try
         {
            ShouldBeIgnored();
            // Passed assertions without exception means it was a match.
            return true;
         }
         catch (AssertionException)
         {
            return false;
         }
      }

      private static string PrepareUrl(string url)
      {
         if (url.StartsWith("~/"))
         {
            return url;
         }

         if (url.StartsWith("/"))
         {
            return "~" + url;
         }

         return "~/" + url;
      }

      private bool ValueCompare(object value1, object value2)
      {
         if (value1 == null & value2 == null)
         {
            return true;
         }

         return value1 is IComparable && value2 is IComparable &&
                StringComparer.InvariantCultureIgnoreCase.Compare(value1.ToString(), value2.ToString()) == 0;
      }

      private Dictionary<string, object> BuildDictionary(object routeValues)
      {
         PropertyInfo[] infos = routeValues.GetType().GetProperties();

         return infos.ToDictionary(info => info.Name, info => info.GetValue(routeValues, null));
      }
   }
}